//  Copyright (c) Microsoft Corporation.  All Rights Reserved.

namespace Common
{
    using System;
    using System.Xml;
    using System.IO;
    using System.Globalization;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Security.Cryptography;
    using System.Runtime.Serialization;
    using System.IdentityModel.Policy;
    using System.IdentityModel.Selectors;
    using System.IdentityModel.Tokens;
    using System.ServiceModel;
    using System.ServiceModel.Security;
    using System.ServiceModel.Security.Tokens;
    using System.ServiceModel.Channels;

    public class RequestSecurityToken : BodyWriter
    {
        string context;
        string tokenType;
        string requestType;
        SecurityToken entropyToken;
        XmlElement rstXml;
        IList<XmlElement> requestProperties;
        byte[] cachedWriteBuffer;
        int cachedWriteBufferLength;
        int keySize;
        SecurityKeyIdentifierClause renewTarget;
        SecurityKeyIdentifierClause closeTarget;
        bool isReceiver;
        bool isReadOnly;
        object appliesTo;
        DataContractSerializer appliesToSerializer;
        Type appliesToType;
        TrustDriver driver;
        object thisLock = new Object();

        public RequestSecurityToken()
            : this(new WSSecurityTokenSerializer())
        {
        }

        public RequestSecurityToken(WSSecurityTokenSerializer securityTokenSerializer)
            : base(false)
        {
            if (securityTokenSerializer == null)
            {
                throw new ArgumentNullException("securityTokenSerializer");
            }
            this.driver = new TrustDriver(new WSTrustTokenSerializer(securityTokenSerializer));
            this.requestType = WSTrust.RequestTypeIssue;
            this.requestProperties = null;
            this.isReceiver = false;
            this.isReadOnly = false;
        }

        public RequestSecurityToken(XmlElement requestSecurityTokenXml,
                                   string context,
                                   string tokenType,
                                   string requestType,
                                   int keySize,
                                   SecurityKeyIdentifierClause renewTarget,
                                   SecurityKeyIdentifierClause closeTarget)
            : this(new TrustDriver(new WSSecurityTokenSerializer()),
                  requestSecurityTokenXml,
                  context,
                  tokenType,
                  requestType,
                  keySize,
                  renewTarget,
                  closeTarget)
        {
        }

        internal RequestSecurityToken(TrustDriver driver,
                                    XmlElement requestSecurityTokenXml,
                                    string context,
                                    string tokenType,
                                    string requestType,
                                    int keySize,
                                    SecurityKeyIdentifierClause renewTarget,
                                    SecurityKeyIdentifierClause closeTarget)
            : base(false)
        {
            if (driver == null)
            {
                throw new ArgumentNullException("driver");
            }
            this.driver = driver;
            if (requestSecurityTokenXml == null)
                throw new ArgumentNullException("rstXml");
            this.rstXml = requestSecurityTokenXml;
            this.context = context;
            this.tokenType = tokenType;
            this.keySize = keySize;
            this.requestType = requestType;
            this.renewTarget = renewTarget;
            this.closeTarget = closeTarget;
            this.isReceiver = true;
            this.isReadOnly = true;
        }

        public string Context
        {
            get
            {
                return this.context;
            }
            set 
            {
                if (this.IsReadOnly)
                    throw new InvalidOperationException("Object is readonly");
                this.context = value;
            }
        }

        public string TokenType
        {
            get 
            {
                return this.tokenType;
            }
            set 
            {
                if (this.IsReadOnly)
                    throw new InvalidOperationException("Object is readonly");
                this.tokenType = value;
            }   
        }

        public int KeySize
        {
            get
            {
                return this.keySize;
            }
            set
            {
                if (this.IsReadOnly)
                    throw new InvalidOperationException("Object is readonly");
                if (value < 0)
                    throw new ArgumentOutOfRangeException("value");
                this.keySize = value;
            }
        }

        public bool IsReadOnly
        {
            get
            {
                return this.isReadOnly;
            }
        }

        public IEnumerable<XmlElement> RequestProperties
        {
            get
            {
                if (this.isReceiver)
                {
                    throw new InvalidOperationException(String.Format("'{0}' is not available in a deserialized RequestSecurityToken" , "RequestProperties"));
                }
                return this.requestProperties;
            }
            set
            {
                if (this.IsReadOnly)
                    throw new InvalidOperationException("Object is readonly");
                if (value != null)
                {
                    int index = 0;
                    Collection<XmlElement> coll = new Collection<XmlElement>();
                    foreach (XmlElement property in value)
                    {
                        if (property == null)
                            throw new ArgumentNullException(String.Format(CultureInfo.InvariantCulture, "value[{0}]", index));
                        coll.Add(property);
                        ++index;
                    }
                    this.requestProperties = coll;
                }
                else
                {
                    this.requestProperties = null;
                }
            }
        }

        public string RequestType
        {
            get
            {
                return this.requestType;
            }
            set
            {
                if (this.IsReadOnly)
                    throw new InvalidOperationException("Object is readonly");
                if (value == null)
                    throw new ArgumentNullException("value");
                this.requestType = value;
            }
        }

        public SecurityKeyIdentifierClause RenewTarget
        {
            get
            {
                return this.renewTarget;
            }
            set
            {
                if (this.IsReadOnly)
                    throw new InvalidOperationException("Object is readonly");
                this.renewTarget = value;
            }
        }

        public SecurityKeyIdentifierClause CloseTarget
        {
            get
            {
                return this.closeTarget;
            }
            set
            {
                if (this.IsReadOnly)
                    throw new InvalidOperationException("Object is readonly");
                this.closeTarget = value;
            }
        }

        public XmlElement RequestSecurityTokenXml
        {
            get
            {
                if (!this.isReceiver)
                {
                    throw new InvalidOperationException(String.Format("'{0}' is only available in a deserialized RequestSecurityToken", "RequestSecurityTokenXml"));
                }
                return this.rstXml;
            }
        }

        internal bool IsReceiver
        {
            get
            {
                return this.isReceiver;
            }
        }

        internal object AppliesTo
        {
            get
            {
                if (this.isReceiver)
                {
                    throw new InvalidOperationException(String.Format("'{0}' is not available in a deserialized RequestSecurityToken", "AppliesTo"));
                }
                return this.appliesTo;
            }
        }

        internal DataContractSerializer AppliesToSerializer
        {
            get
            {
                if (this.isReceiver)
                {
                    throw new InvalidOperationException(String.Format("'{0}' is not available in a deserialized RequestSecurityToken", "AppliesToSerializer"));
                }
                return this.appliesToSerializer;
            }
        }

        internal Type AppliesToType
        {
            get
            {
                if (this.isReceiver)
                {
                    throw new InvalidOperationException(String.Format("'{0}' is not available in a deserialized RequestSecurityToken", "AppliesToType"));
                }
                return this.appliesToType;
            }
        }

        protected Object ThisLock
        {
            get
            {
                return this.thisLock;
            }
        }

        public SecurityToken GetRequestorEntropy()
        {
            return this.GetRequestorEntropy(null);
        }

        internal SecurityToken GetRequestorEntropy(SecurityTokenResolver resolver) 
        {
            if (this.isReceiver)
            {
                return this.driver.GetEntropy(this, resolver);
            }
            else
                return this.entropyToken;
        }

        public void SetRequestorEntropy(byte[] entropy)
        {
            if (this.IsReadOnly)
                throw new InvalidOperationException("Object is readonly");
            this.entropyToken = (entropy != null) ? new NonceToken(entropy) : null;
        }

        public void SetAppliesTo<T>(T appliesTo, DataContractSerializer serializer)
        {
            if (this.IsReadOnly)
                throw new InvalidOperationException("Object is readonly");
            if (appliesTo != null && serializer == null)
            {
                throw new ArgumentNullException("serializer");
            }
            this.appliesTo = appliesTo;
            this.appliesToSerializer = serializer;
            this.appliesToType = typeof(T);
        }

        public void GetAppliesToQName(out string localName, out string namespaceUri)
        {
            if (!this.isReceiver)
                throw new InvalidOperationException(String.Format("'{0}' is only available in a deserialized RequestSecurityToken", "MatchesAppliesTo"));
            this.driver.GetAppliesToQName(this, out localName, out namespaceUri);
        }

        public T GetAppliesTo<T>()
        {
            return this.GetAppliesTo<T>(new DataContractSerializer(typeof(T)));
        }

        public T GetAppliesTo<T>(XmlObjectSerializer serializer)
        {
            if (this.isReceiver)
            {
                if (serializer == null)
                {
                    throw new ArgumentNullException("serializer");
                }
                return this.driver.GetAppliesTo<T>(this, serializer);
            }
            else
            {
                return (T)this.appliesTo;
            }
        }

        void OnWriteTo(XmlWriter writer)
        {
            if (this.isReceiver)
            {
                this.rstXml.WriteTo(writer);
            }
            else
            {
                this.driver.WriteRequestSecurityToken(this, writer);
            }
        }

        public void WriteTo(XmlWriter writer)
        {
            if (writer == null)
                throw new ArgumentNullException("writer");
            if (this.IsReadOnly)
            {
                // cache the serialized bytes to ensure repeatability
                if (this.cachedWriteBuffer == null)
                {
                    MemoryStream stream = new MemoryStream();
                    using (XmlDictionaryWriter binaryWriter = XmlDictionaryWriter.CreateBinaryWriter(stream, null))
                    {
                        this.OnWriteTo(binaryWriter);
                        binaryWriter.Flush();
                        stream.Flush();
                        stream.Seek(0, SeekOrigin.Begin);
                        this.cachedWriteBuffer = stream.GetBuffer();
                        this.cachedWriteBufferLength = (int)stream.Length;
                    }
                }
                writer.WriteNode(XmlDictionaryReader.CreateBinaryReader(this.cachedWriteBuffer, 0, this.cachedWriteBufferLength, XmlDictionaryReaderQuotas.Max), false);
            }
            else
                this.OnWriteTo(writer);
        }

        public static RequestSecurityToken CreateFrom(XmlReader reader) 
        {
            return CreateFrom(new WSSecurityTokenSerializer(), reader);
        }

        public static RequestSecurityToken CreateFrom(WSSecurityTokenSerializer tokenSerializer,  XmlReader reader)
        {
            return new TrustDriver(tokenSerializer).CreateRequestSecurityToken(reader);
        }

        public void MakeReadOnly()
        {
            if (!this.isReadOnly)
            {
                this.isReadOnly = true;
                if (this.requestProperties != null)
                {
                    this.requestProperties = new ReadOnlyCollection<XmlElement>(this.requestProperties);
                }
                this.OnMakeReadOnly();
            }
        }

        internal protected virtual void OnWriteCustomAttributes(XmlWriter writer) { }

        internal protected virtual void OnWriteCustomElements(XmlWriter writer) { }

        internal protected virtual void OnMakeReadOnly() { }

        protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
        {
            WriteTo(writer);
        }
    }
}
